package com.quectel.util.client.minio;

import cn.hutool.core.io.FileUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.quectel.util.ex.CheckErrorException;
import com.quectel.util.ex.kit.MinIOException;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * MinIO客户端
 *
 * @author: caolu
 * @email: louis.cao@quectel.com
 * @date: 2023-04-25 19:19:00
 */
public class MinIOClient {

    public static final String DEFAULT_PUBLIC_BUCKET = "public-bucket";

    public static final String DEFAULT_PRIVATE_BUCKET = "private-bucket";


    /**
     * 内网url
     */
    private String internalEndpoint = "http://localhost:9000/";

    /**
     * 外网url
     */
    private String externalEndpoint = "https://minio-storage.example.com:9000/";

    /**
     * 接入key
     */
    private String accessKey = "admin";

    /**
     * 接入密钥
     */
    private String secretKey = "admin";

    /**
     * 内网实例用于上传
     */
    private MinioClient internalMinioClient = null;

    /**
     * 外网实例用于获取url
     */
    private MinioClient externalMinioClient = null;


    /**
     * 无参构造
     */
    private MinIOClient() {

    }

    /**
     * 有参构造
     *
     * @param internalEndpoint
     * @param externalEndpoint
     * @param accessKey
     * @param secretKey
     */
    public MinIOClient(String internalEndpoint, String externalEndpoint, String accessKey, String secretKey) {

        if (internalEndpoint == null || !internalEndpoint.endsWith("/")) {
            throw new CheckErrorException("MinIO internal endpoint address must be not null and need end width '/'");
        }
        if (externalEndpoint == null || !externalEndpoint.endsWith("/")) {
            throw new CheckErrorException("MinIO external endpoint address must be not null and need end width '/'");
        }
        if (accessKey == null) {
            throw new CheckErrorException("MinIO accessKey must be not null");
        }
        if (secretKey == null) {
            throw new CheckErrorException("MinIO secretKey must be not null");
        }
        this.internalEndpoint = internalEndpoint;
        this.externalEndpoint = externalEndpoint;
        this.accessKey = accessKey;
        this.secretKey = secretKey;

        //内网实例
        internalMinioClient = MinioClient.builder()
                .endpoint(internalEndpoint)
                .credentials(accessKey, secretKey)
                .build();

        //外网实例
        externalMinioClient = MinioClient.builder()
                .endpoint(externalEndpoint)
                .credentials(accessKey, secretKey)
                .build();

        //创建默认公开桶
        makeNotFullPublicBucket(DEFAULT_PUBLIC_BUCKET);
        //创建默认私有桶
        makePrivateBucket(DEFAULT_PRIVATE_BUCKET);

    }


    private static String getStorageDir() {
        return new SimpleDateFormat("yyyy/MM/dd/").format(new Date());
    }

    /**
     * 快速上传 该方法会返回永久外链
     *
     * @param bucketName   必须设置为public 否则外链只有7天 为空默认为public-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param file         要上传的文件
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @return 公网的地址
     */
    public String fatPutObjectToPublicBucketObtainUrl(String bucketName, String businessFlag, File file, boolean keepFileName) {
        try {
            String realBucketName = bucketName == null ? DEFAULT_PUBLIC_BUCKET : bucketName;
            String objectName = fastPutObjectToBucketObtainObject(realBucketName, businessFlag, file, keepFileName);
            return externalEndpoint + realBucketName + "/" + objectName;
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 该方法会返回永久外链
     *
     * @param bucketName   必须设置为public 否则外链只有7天 为空默认为public-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param file         要上传的文件
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @return 公网的地址
     */
    public String putObjectToPublicBucketObtainUrl(String bucketName, String businessFlag, File file, boolean keepFileName) {
        try {
            String realBucketName = bucketName == null ? DEFAULT_PUBLIC_BUCKET : bucketName;
            String objectName = putObjectToBucketObtainObject(realBucketName, businessFlag, file, keepFileName);
            return externalEndpoint + realBucketName + "/" + objectName;
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 快速上传 该方法会返回永久外链
     *
     * @param bucketName   必须设置为public 否则外链只有7天 为空默认为public-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param in           文件流 上传完毕会关闭流
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @return 公网的地址
     */
    public String fastPutStreamToPublicBucketObtainUrl(String bucketName, String businessFlag, InputStream in, String fileName, boolean keepFileName) {
        File tmpDir = null;
        File tempFile = null;
        try (InputStream is = in) {
            String realBucketName = bucketName == null ? DEFAULT_PUBLIC_BUCKET : bucketName;
            tmpDir = new File(FileUtil.getTmpDir().getAbsolutePath() + UUID.randomUUID());
            tempFile = new File(tmpDir, fileName);
            FileUtil.writeFromStream(is, tempFile);

            String objectName = fastPutObjectToBucketObtainObject(realBucketName, businessFlag, tempFile, keepFileName);
            return externalEndpoint + realBucketName + "/" + objectName;
        } catch (Exception e) {
            throw new MinIOException(e);
        } finally {
            FileUtil.del(tempFile);
            FileUtil.del(tmpDir);
        }
    }

    /**
     * 该方法会返回永久外链
     *
     * @param bucketName   必须设置为public 否则外链只有7天 为空默认为public-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param in           文件流 上传完毕会关闭流
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @return 公网的地址
     */
    public String putStreamToPublicBucketObtainUrl(String bucketName, String businessFlag, InputStream in, String fileName, boolean keepFileName) {
        File tmpDir = null;
        File tempFile = null;
        try (InputStream is = in) {
            String realBucketName = bucketName == null ? DEFAULT_PUBLIC_BUCKET : bucketName;
            tmpDir = new File(FileUtil.getTmpDir().getAbsolutePath() + UUID.randomUUID());
            tempFile = new File(tmpDir, fileName);
            FileUtil.writeFromStream(is, tempFile);

            String objectName = putObjectToBucketObtainObject(realBucketName, businessFlag, tempFile, keepFileName);
            return externalEndpoint + realBucketName + "/" + objectName;
        } catch (Exception e) {
            throw new MinIOException(e);
        } finally {
            FileUtil.del(tempFile);
            FileUtil.del(tmpDir);
        }
    }

    /**
     * 快速上传返回文件在该bucket下的标识
     *
     * @param bucketName   bucket名称 为空默认为private-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param file         要上传的文件
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @return object将来可以下载或者获取临时外链使用
     */
    public String fastPutObjectToBucketObtainObject(String bucketName, String businessFlag, File file, boolean keepFileName) {
        try {
            String suffix = FileUtil.getSuffix(file);
            if (StringUtils.isBlank(suffix)) {
                suffix = ".file";
            } else {
                suffix = "." + suffix;
            }

            StringBuilder sb = new StringBuilder(businessFlag + "/");
            byte[] bytes = DigestUtil.md5(file);
            for (int i = 0; i < bytes.length; i++) {
                sb.append(Hex.encodeHexString(new byte[]{bytes[i]})).append("/");
            }
            String shaHex = Hex.encodeHexString(bytes);

            String object = sb + (keepFileName ? (shaHex + "/" + file.getName()) : (shaHex + suffix));
            if (objectExists(bucketName == null ? DEFAULT_PRIVATE_BUCKET : bucketName, object)) {
                return object;
            }
            internalMinioClient.uploadObject(
                    UploadObjectArgs.builder()
                            .bucket(bucketName == null ? DEFAULT_PRIVATE_BUCKET : bucketName)
                            .object(object)
                            .filename(file.getAbsolutePath())
                            .build()
            );
            return object;
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 返回文件在该bucket下的标识
     *
     * @param bucketName   bucket名称 为空默认为private-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param file         要上传的文件
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @return object将来可以下载或者获取临时外链使用
     */
    public String putObjectToBucketObtainObject(String bucketName, String businessFlag, File file, boolean keepFileName) {
        try {
            String suffix = FileUtil.getSuffix(file);
            if (StringUtils.isBlank(suffix)) {
                suffix = ".file";
            } else {
                suffix = "." + suffix;
            }
            String object = businessFlag + "/" + getStorageDir() + (keepFileName ? file.getName() : (UUID.randomUUID().toString().replace("-", "") + suffix));
            internalMinioClient.uploadObject(
                    UploadObjectArgs.builder()
                            .bucket(bucketName == null ? DEFAULT_PRIVATE_BUCKET : bucketName)
                            .object(object)
                            .filename(file.getAbsolutePath())
                            .build()
            );
            return object;
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 获取文件流
     *
     * @param bucketName bucket名称 为null为默认的bucket private-bucket
     * @param object     putObjectToBucket返回的结果
     * @return 一定要注意关闭
     */
    public InputStream getObject(String bucketName, String object) {

        // get object given the bucket and object name
        try {
            return internalMinioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(object)
                            .build());
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 获取临时url有有效期
     *
     * @param bucketName bucket名称 为null为默认的bucket private-bucket
     * @param object     上传的文件对象
     * @param duration
     * @param timeUnit
     * @return 一定要注意关闭
     */
    public String makeObjectTemporaryUrl(String bucketName, String object, int duration, TimeUnit timeUnit) {
        try {
            return externalMinioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName == null ? DEFAULT_PUBLIC_BUCKET : bucketName)
                            .object(object)
                            .expiry(duration, timeUnit)
                            .build());
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 获取临时url有有效期
     *
     * @param bucketName   bucket名称 为null为默认的bucket private-bucket
     * @param businessFlag   业务类型 如car-access不允许传入/car-access/
     * @param file         要上传的文件
     * @param keepFileName 是否保持原来文件名 会覆盖文件 请慎用
     * @param duration
     * @param timeUnit
     * @return 一定要注意关闭
     */
    public String putObjectToBucketObtainTemporaryUrl(String bucketName, String businessFlag, File file, boolean keepFileName, int duration, TimeUnit timeUnit) {
        try {
            return externalMinioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName == null ? DEFAULT_PUBLIC_BUCKET : bucketName)
                            .object(putObjectToBucketObtainObject(bucketName, businessFlag, file, keepFileName))
                            .expiry(duration, timeUnit)
                            .build());
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }


    /**
     * 根据object或者url进行删除(批量)
     *
     * @param bucketName    bucket名称 为null为默认的bucket private-bucket
     * @param objectsOrUrls 批量的object标识
     * @return 返回成功的数量
     */
    public int deleteObjects(String bucketName, List<String> objectsOrUrls) {
        if (bucketName == null || objectsOrUrls == null || objectsOrUrls.size() <= 0) {
            throw new CheckErrorException();
        }
        List<DeleteObject> deleteObjects = new LinkedList<>();
        for (String objectOrUrl : objectsOrUrls) {
            if (objectOrUrl.contains(bucketName)) {
                int i = objectOrUrl.indexOf(bucketName);
                deleteObjects.add(new DeleteObject(objectOrUrl.substring(i + bucketName.length() + 1)));
            } else {
                deleteObjects.add(new DeleteObject(objectOrUrl));
            }
        }
        try {
            int i = objectsOrUrls.size();
            Iterable<Result<DeleteError>> results =
                    internalMinioClient.removeObjects(
                            RemoveObjectsArgs.builder().bucket(bucketName).objects(deleteObjects).build());
            for (Result<DeleteError> result : results) {
                --i;
            }
            return i;
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 创建不完全公开桶，阉割了action中的s3:ListBucket保障桶的安全性(非常推荐)
     *
     * @param bucket
     */
    public void makeNotFullPublicBucket(String bucket) {

        try {
            makePrivateBucket(bucket);

            //如果策略是private或者action中包含s3:ListBucket (并不是真正的public 会阉割Action中的"s3:ListBucket")
            String notFullPublicPolicy = String.format(
                    "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucketMultipartUploads\"],\"Resource\":[\"arn:aws:s3:::%s\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\",\"s3:ListMultipartUploadParts\"],\"Resource\":[\"arn:aws:s3:::%s/*\"]}]}"
                    , bucket, bucket
            );
            internalMinioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucket).config(notFullPublicPolicy).build());


        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }


    /**
     * 创建一个私有桶
     *
     * @param bucket
     */
    private void makePrivateBucket(String bucket) {
        if (!bucketExists(bucket)) {
            try {
                internalMinioClient.makeBucket(
                        MakeBucketArgs.builder().bucket(bucket).build()
                );
            } catch (Exception e) {
                throw new MinIOException(e);
            }
        }
    }

    /**
     * 得到桶策略
     *
     * @param bucket
     * @return
     */
    public String getBucketPolicy(String bucket) {
        try {
            return internalMinioClient.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucket).build());
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }

    /**
     * 文件是否存在
     *
     * @param bucket
     * @param object
     * @return
     */
    public boolean objectExists(String bucket, String object) {
        try {
            internalMinioClient.statObject(
                    StatObjectArgs.builder().bucket(bucket).object(object).build());
            return true;
        } catch (Exception e) {

        }
        return false;
    }

    /**
     * 查询桶是否存在
     *
     * @param bucket
     * @return
     */
    public boolean bucketExists(String bucket) {
        try {
            return internalMinioClient.bucketExists(
                    BucketExistsArgs.builder().bucket(bucket).build()
            );
        } catch (Exception e) {
            throw new MinIOException(e);
        }
    }
}
